package cmd

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"github.com/SAP/jenkins-library/pkg/command"
	piperhttp "github.com/SAP/jenkins-library/pkg/http"
	"github.com/SAP/jenkins-library/pkg/log"
	"github.com/SAP/jenkins-library/pkg/telemetry"
	"github.com/pkg/errors"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

var open = _open
var getSHA256 = _getSHA256

func _open(path string) (io.ReadCloser, error) {
	return os.Open(path)
}

type malwareExecuteScanResponse struct {
	MalwareDetected          bool
	EncryptedContentDetected bool
	ScanSize                 int
	MimeType                 string
	SHA256                   string
}

func malwareExecuteScan(config malwareExecuteScanOptions, telemetryData *telemetry.CustomData) {
	// for command execution use Command
	c := command.Command{}
	// reroute command output to logging framework
	c.Stdout(log.Writer())
	c.Stderr(log.Writer())

	// for http calls import  piperhttp "github.com/SAP/jenkins-library/pkg/http"
	// and use a  &piperhttp.Client{} in a custom system
	// Example: step checkmarxExecuteScan.go

	httpClient := &piperhttp.Client{}

	// error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end
	err := runMalwareScan(&config, telemetryData, &c, httpClient)
	if err != nil {
		log.Entry().WithError(err).Fatal("step execution failed")
	}
}

func runMalwareScan(config *malwareExecuteScanOptions, telemetryData *telemetry.CustomData, command command.ExecRunner,
	httpClient piperhttp.Sender) error {

	log.Entry().Infof("Scanning file \"%s\" for malware using service \"%s\"", config.File, config.Host)

	candidate, err := open(config.File)
	if err != nil {
		return err
	}
	defer candidate.Close()

	timeout, err := time.ParseDuration(fmt.Sprintf("%ss", config.Timeout))
	if err != nil {
		return errors.Wrapf(err, "Invalid timeout: %v", config.Timeout)
	}

	opts := piperhttp.ClientOptions{
		Username:           config.Username,
		Password:           config.Password,
		MaxRequestDuration: timeout,
		TransportTimeout:   timeout,
	}
	httpClient.SetOptions(opts)

	var scanResponse *malwareExecuteScanResponse
	scanResponse, err = sendMalwareScanRequest(httpClient, "POST", config.Host+"/scan", candidate)

	if err != nil {
		return err
	}

	log.Entry().Debugf(
		"File '%s' has been scanned. MalwareDetected: %t, EncryptedContentDetected: %t, ScanSize: %d, MimeType: '%s', SHA256: '%s'",
		config.File,
		scanResponse.MalwareDetected,
		scanResponse.EncryptedContentDetected,
		scanResponse.ScanSize,
		scanResponse.MimeType,
		scanResponse.SHA256)

	err = validateHash(scanResponse.SHA256, config.File)
	if err != nil {
		return err
	}

	if scanResponse.MalwareDetected || scanResponse.EncryptedContentDetected {
		return fmt.Errorf("Malware scan failed for file '%s'. Malware detected: %t, encrypted content detected: %t",
			config.File, scanResponse.MalwareDetected, scanResponse.EncryptedContentDetected)
	}

	log.Entry().Infof("Malware scan succeeded for file '%s'. Malware detected: %t, encrypted content detected: %t",
		config.File, scanResponse.MalwareDetected, scanResponse.EncryptedContentDetected)

	return nil
}

func sendMalwareScanRequest(client piperhttp.Sender, method, url string, candidate io.Reader) (*malwareExecuteScanResponse, error) {

	// piper http utils mashall some http response codes into errors. We wan't to check the status code
	// ourselvs hence we wait with returning that error (maybe also related to errors others than http status codes)

	// sendRequest results in any combination of nil and non-nil response and error.
	// a response body could even be already closed.
	response, err := client.SendRequest(method, url, candidate, prepareHeaders(), nil)

	if response != nil && response.Body != nil {
		defer response.Body.Close()
	}

	return validateResponse(response, err)
}

func validateResponse(response *http.Response, err error) (*malwareExecuteScanResponse, error) {

	var body []byte
	var errRead error
	if response != nil && response.Body != nil {
		body, errRead = ioutil.ReadAll(response.Body)
	}

	if err != nil {
		return nil, fmt.Errorf("HTTP request failed with error: %v. Details: \"%s\"", err, body)
	}

	if response == nil {
		return nil, fmt.Errorf("No response available")
	}

	if response.StatusCode != 200 {
		return nil, fmt.Errorf("Unexpected response code (%d). %d expected. Details: \"%s\"", response.StatusCode, 200, body)
	}

	if errRead != nil {
		return nil, errRead
	}

	return marshalResponse(body)
}

func marshalResponse(body []byte) (*malwareExecuteScanResponse, error) {

	var scanResponse malwareExecuteScanResponse

	err := json.Unmarshal(body, &scanResponse)

	if err != nil {
		return nil, errors.Wrap(err, fmt.Sprintf("Unmarshalling of response body failed. Body: '%s'", body))
	}

	return &scanResponse, nil
}

func validateHash(remoteHash, fileName string) error {

	hash, err := getSHA256(fileName)
	if err != nil {
		return err
	}

	if hash == remoteHash {
		log.Entry().Infof("Hash returned from malwarescan service matches file hash for file '%s' (%s)", fileName, hash)
	} else {
		return fmt.Errorf("Hash returned from malwarescan service ('%s') does not match file hash ('%s') for file '%s'",
			remoteHash, hash, fileName)
	}

	return nil
}

func _getSHA256(fileName string) (string, error) {

	f, err := open(fileName)
	if err != nil {
		return "", err
	}
	defer f.Close()

	hash := sha256.New()
	_, err = io.Copy(hash, f)
	if err != nil {
		return "", err
	}

	return fmt.Sprintf("%x", string(hash.Sum(nil))), nil
}

func prepareHeaders() http.Header {
	headers := http.Header{}
	headers.Add("Content-Type", "application/octet-stream")
	return headers
}
